/**
 * 
 */
package ro.dta.idbi.model;

import java.util.ArrayList;
import java.util.List;

import uk.ac.bbk.dcs.automed.qproc.QProcException;
import uk.ac.bbk.dcs.automed.qproc.QueryProcessor;
import uk.ac.bbk.dcs.automed.qproc.QueryProcessorConfiguration;
import uk.ac.bbk.dcs.automed.qproc.iql.ASG;
import uk.ac.bbk.dcs.automed.qproc.iql.ASGResult;
import uk.ac.bbk.dcs.automed.qproc.iql.ParseException;
import uk.ac.ic.doc.automed.InconsistentException;
import uk.ac.ic.doc.automed.IntegrityException;
import uk.ac.ic.doc.automed.NotFoundException;
import uk.ac.ic.doc.automed.reps.Schema;
import uk.ac.ic.doc.automed.reps.SchemaObject;
import uk.ac.ic.doc.automed.reps.SchemasNotIdenticalException;
import uk.ac.ic.doc.automed.reps.Transformation;
import uk.ac.ic.doc.automed.wrappers.AutoMedWrapper;
import uk.ac.ic.doc.automed.wrappers.AutoMedWrapperFactory;
import uk.ac.ic.doc.automed.wrappers.DataSourceException;
import uk.ac.ic.doc.automed.wrappers.SSDFunctionTable;

/**
 * Utilities for the AutoMed library
 * 
 * @author Tudor Dobrila
 */
public final class AutoMedUtils {

	public static QueryProcessor gqp;

	public static QueryProcessorConfiguration qpc;

	static {
		// Initialise the query processor
		qpc = new QueryProcessorConfiguration();
		qpc.setRangeSemantics(QueryProcessorConfiguration.RANGE_LOWER);
		qpc.addFunctionTable(new SSDFunctionTable());
		qpc.setReformulationSemantics(QueryProcessorConfiguration.REFORMULATION_GAV);
		qpc.setOptimisationState(true);
		qpc.setCacheStatus(true);
		qpc.setThreadingLevel(0);
		qpc.setIncrementalProcessing(0);
		qpc.setBagSemanticsAllowed(false);
		gqp = new QueryProcessor();
	}

	/**
	 * Wrap a schema into the AutoMed repository
	 * 
	 * @param username
	 *            Username for the data source
	 * @param password
	 *            Password for the data source
	 * @param driver
	 *            Driver used by the data source
	 * @param urlbase
	 *            URL Location of the data source
	 * @param wrapFactory
	 *            Wrapper factory used in the wrapping process
	 * @param dbName
	 *            Database name
	 * @param name
	 *            Desired name of the database in the AutoMed repository
	 * @return Resulting schema
	 */
	public static Schema wrapSchema(final String username, final String password,
			final String driver, final String urlbase, final AutoMedWrapperFactory wrapFactory,
			final String dbName, final String name) throws IntegrityException, NotFoundException,
			DataSourceException {

		// Check if schema already exists, remove it if so
		final String sourceName = name + "_src";
		if (Schema.exists(name)) {
			Schema.getSchema(name).retract();
		}
		if (Schema.exists(sourceName)) {
			Schema.getSchema(sourceName).retract();
		}

		// Get the Source oriented schema
		final AutoMedWrapper wrapper = AutoMedWrapper.selectNewAutoMedWrapper(username, password,
				null, driver, urlbase + dbName, sourceName, wrapFactory);

		// Generate an AutoMed oriented schema
		return wrapper.newAutoMedSchema(name);
	}

	/**
	 * Run an IQL query against a schema in the AutoMed repository
	 * 
	 * @param schema
	 *            Schema that will be queried
	 * @param query
	 *            IQL query
	 * @return Result of the query
	 */
	public static QueryResult runIqlQuery(final Schema schema, final String query)
			throws ParseException, QProcException, IntegrityException {
		qpc.setOptimisationState(true);
		qpc.setIncrementalProcessing(0);
		Schema[] extSchemas = schema.getAttachedSchemas(Schema.DATA_SOURCE_TYPE);
		if (extSchemas.length == 0) {
			throw new QProcException("No attached data sources");
		}
		final long start = System.currentTimeMillis();
		final ASG graph = new ASG(query);
		gqp.process(graph, schema, extSchemas, null, qpc);
		return new QueryResult(graph, null, (System.currentTimeMillis() - start) / 1000.0);
	}

	/**
	 * Run an IQL query against a schema in the AutoMed repository
	 * 
	 * @param schema
	 *            Schema that will be queried
	 * @param query
	 *            IQL query
	 * @return Result of the query
	 */
	public static QueryResult runIqlQueryIncrementally(final Schema schema, final String query,
			int resultsShown) throws ParseException, QProcException, IntegrityException {
		QueryProcessor qp = new QueryProcessor();
		QueryProcessorConfiguration qpc = new QueryProcessorConfiguration();
		qpc.setRangeSemantics(QueryProcessorConfiguration.RANGE_LOWER);
		qpc.addFunctionTable(new SSDFunctionTable());
		qpc.setReformulationSemantics(QueryProcessorConfiguration.REFORMULATION_GAV);
		qpc.setOptimisationState(false);
		qpc.setCacheStatus(true);
		qpc.setThreadingLevel(0);
		qpc.setBagSemanticsAllowed(false);
		qpc.setIncrementalProcessing(resultsShown);
		Schema[] extSchemas = schema.getAttachedSchemas(Schema.DATA_SOURCE_TYPE);
		if (extSchemas.length == 0) {
			throw new QProcException("No attached data sources");
		}
		final long start = System.currentTimeMillis();
		final ASG graph = new ASG(query);
		qp.processIncrementally(graph, schema, extSchemas, null, qpc);
		final ASGResult result = new ASGResult(qp);
		return new QueryResult(graph, result, (System.currentTimeMillis() - start) / 1000.0);
	}

	/**
	 * Merge a list of schemas
	 * 
	 * @param schemas
	 *            Schemas to be merged
	 * @return Resulting schema after merging all schemas
	 * @throws NotFoundException
	 *             Thrown if one of the schemas does not exist
	 */
	public static Schema mergeSchemas(final List<String> schemas) throws NotFoundException {
		if (schemas.isEmpty()) {
			throw new IllegalArgumentException("Expected one or more schemas. Got zero schemas.");
		}

		Schema lastSchema = Schema.getSchema(schemas.get(0));
		for (int i = 1; i < schemas.size(); i++) {
			final Schema crtSchema = Schema.getSchema(schemas.get(i));
			final Schema[] result = match(lastSchema, crtSchema);
			lastSchema = result[1];
		}

		return lastSchema;
	}

	/**
	 * Match two schemas
	 * 
	 * @param s1
	 *            Schema 1
	 * @param s2
	 *            Schema 2
	 * @return Final schema after matching the two schemas
	 */
	public static Schema[] match(Schema s1, Schema s2) {
		Schema s1p = s1;
		Schema s2p = s2;
		SchemaObject[] s2so = s2.getSchemaObjects();
		SchemaObject[] s1so = s1.getSchemaObjects();
		try {
			for (int i = 0; i < s2so.length; ++i) {
				String scheme = s2so[i].getSchemeNamesString();
				if (!(s1.contains(scheme))) {
					if (s2so[i].getConstruct().equals(AbstractPattern.column)) {
						Object[] schemeDef = s2so[i].getSchemeDefinition();
						schemeDef[0] = s1p.getSchemaObject(schemeDef[0].toString());
						s1p = s1p.applyExtendTransformation(s2so[i].getConstruct(), schemeDef,
								null, null);
					} else {
						s1p = s1p.applyExtendTransformation(s2so[i].getConstruct(), scheme, null,
								null);
					}
				}
			}

			for (int i = 0; i < s1so.length; ++i) {
				String scheme = s1so[i].getSchemeNamesString();
				if (!(s2.contains(scheme))) {
					if (s1so[i].getConstruct().equals(AbstractPattern.column)) {
						Object[] schemeDef = s1so[i].getSchemeDefinition();
						schemeDef[0] = s2p.getSchemaObject(schemeDef[0].toString());
						s2p = s2p.applyExtendTransformation(s1so[i].getConstruct(), schemeDef,
								null, null);
					} else {
						s2p = s2p.applyExtendTransformation(s1so[i].getConstruct(), scheme, null,
								null);
					}
				}
			}
			Transformation t = Transformation.createIdentTransformation(s1p, s2p, "(++)", null);
			t.setProperty(Transformation.PROPERTY_IQL_FUNCTION, "(++)");
		} catch (SchemasNotIdenticalException e) {
			throw InconsistentException.newInconsistentException(
					"Match generate non-identical schemas", e);
		} catch (Exception te) {
			throw InconsistentException.newInconsistentException(
					"Match generated invalid transformations", te);
		}
		return new Schema[] { s1p, s2p };
	}

	/**
	 * Get a list of all names of columns in a given table
	 * 
	 * @param crtSchema
	 *            Schema used to re
	 * @param crtTbl
	 *            Table for which to retrieve columns
	 * @return List of column names
	 */
	public static List<String> getColumns(Schema crtSchema, SchemaObject crtTbl) {
		SchemaObject[] cols = crtSchema.getDependentSchemaObjects(crtTbl, AbstractPattern.column);
		List<String> result = new ArrayList<String>();

		for (SchemaObject c : cols) {
			result.add(c.getValue());
		}

		return result;
	}

	/**
	 * Get the primary keys of a table
	 * 
	 * @param schema
	 *            Schema containing the table
	 * @param table
	 *            Schema object representing the table
	 * @return Columns part of the primary key as string
	 */
	public static List<String> getPK(final Schema schema, final SchemaObject table) {
		final List<String> pks = new ArrayList<String>();
		final SchemaObject[] depCol = schema.getDependentSchemaObjects(table,
				AbstractPattern.primaryKey);

		if (depCol.length != 0) {
			final Object[] pkObj = depCol[0].getSchemeDefinition();
			for (int i = 2; i < pkObj.length; i++) {
				pks.add(((SchemaObject) pkObj[i]).getValue());
			}
		}

		return pks;
	}

	/**
	 * Get all supertype tables of a given table
	 * 
	 * @param schema
	 *            Schema containing the table
	 * @param table
	 *            Table for which the supertypes must be retrieved
	 * @return List of supertype tables of the given table
	 */
	public static List<String> getSupertypes(final Schema schema, final SchemaObject table) {
		final List<String> res = new ArrayList<String>();
		final SchemaObject[] fks = schema.getDependentSchemaObjects(table,
				AbstractPattern.foreignKey);
		final List<String> pks = getPK(schema, table);

		for (SchemaObject fk : fks) {
			final Object[] fkDef = fk.getSchemeDefinition();
			final String source = ((SchemaObject) fkDef[1]).getValue();

			if (!source.equals(table.getValue())) {
				continue;
			}

			final int numCols = (fkDef.length - 3) / 2;
			if (numCols != pks.size()) {
				continue;
			}

			final List<String> targetPk = AutoMedUtils.getPK(schema,
					(SchemaObject) fkDef[2 + numCols]);
			boolean isOk = true;
			for (int i = 0; i < numCols; i++) {
				final String crtCol = ((SchemaObject) fkDef[2 + i]).getValue();
				if (!pks.contains(crtCol)) {
					isOk = false;
				}

				final SchemaObject targetCol = (SchemaObject) fkDef[3 + numCols + i];
				if (!targetPk.contains(targetCol.getValue())) {
					isOk = false;
				}
			}

			if (isOk) {
				final String target = ((SchemaObject) fkDef[2 + numCols]).getValue();
				res.add(target);
			}
		}

		return res;
	}

	/**
	 * Get the primary keys of a table as a string, enclosed in curly brackets
	 * if more than one column is part of the primary key
	 * 
	 * @param schema
	 *            Schema containing the table
	 * @param table
	 *            Schema object representing the table
	 * @return Columns part of the primary key as string
	 */
	public static String getPKString(final Schema schema, final SchemaObject table) {
		final List<String> pklist = AutoMedUtils.getPK(schema, table);
		String pkStr;
		if (pklist.size() > 1) {
			final StringBuilder builder = new StringBuilder("{");
			for (String pk : pklist) {
				builder.append(pk).append(", ");
			}
			builder.setLength(builder.length() - 2);
			builder.append("}");
			pkStr = builder.toString();
		} else if (pklist.size() == 1) {
			pkStr = pklist.get(0);
		} else {
			throw new IllegalArgumentException("Source table does not have a PK.");
		}

		return pkStr;
	}

	/**
	 * Convert an IQL query to Latex format
	 * 
	 * @param iql
	 *            Iql query to convert
	 * @return Iql query in Latex format
	 */
	public static String toLatex(final String iql) {
		String iql2 = iql;
		iql2 = iql2.replace("<<", "$\\ll$");
		iql2 = iql2.replace(">>", "$\\gg$");
		iql2 = iql2.replace("<-", "$\\leftarrow$");
		iql2 = iql2.replace("{", "$\\lbrace$");
		iql2 = iql2.replace("}", "$\\rbrace$");
		iql2 = iql2.replace("_", "\\_");
		iql2 = iql2.replace("|", "$\\vert$");
		iql2 = iql2.replace("$$", " ");
		return iql2;
	}

	/**
	 * Get the Foreign Key constraint used in marking <i>child</i> as a subset
	 * of <i>parent</i><br />
	 * 
	 * Done by finding the foreign key from the columns in the PK of
	 * <i>child</i> to columns in the PK of <i>parent</i>
	 * 
	 * @param schema
	 *            Schema in the context of which the FK is searched for
	 * @param parent
	 *            Table marked as parent
	 * @param child
	 *            Table marked as child
	 * @return The FK between the parent and child, if one exists, null
	 *         otherwise
	 */
	public static SchemaObject getSupertypeFK(Schema schema, SchemaObject parent, SchemaObject child) {
		List<String> childPK = AutoMedUtils.getPK(schema, child);
		List<String> parentPK = AutoMedUtils.getPK(schema, parent);

		return getForeignKey(schema, child, parent, childPK, parentPK);
	}

	/**
	 * Get the Foreign Key constraint between <i>source</i> and <i>target</i>
	 * from the columns in <i>sourceCols</i> to the columns in <i>targetCols</i>
	 * 
	 * 
	 * @param schema
	 *            Schema in the context of which the FK is searched for
	 * @param source
	 *            Source table
	 * @param target
	 *            Target table
	 * @param sourceCols
	 *            Columns in the source table being part of the FK
	 * @param targetCols
	 *            Columns in the target table being part of the FK
	 * @return The FK between the source and target, if one exists, null
	 *         otherwise
	 */
	public static SchemaObject getForeignKey(Schema schema, SchemaObject source,
			SchemaObject target, List<String> sourceCols, List<String> targetCols) {
		SchemaObject[] fkObjs = schema
				.getDependentSchemaObjects(source, AbstractPattern.foreignKey);
		SchemaObject fk1 = null;

		if (sourceCols.size() == targetCols.size()) {
			for (SchemaObject fkObj : fkObjs) {
				Object[] fkObjDef = fkObj.getSchemeDefinition();
				int expLen = 3 + sourceCols.size() * 2;

				if (fkObjDef.length != expLen) {
					continue;
				}

				SchemaObject sTbl = (SchemaObject) fkObjDef[1];
				if (!sTbl.getValue().equals(source.getValue())) {
					continue;
				}
				SchemaObject tTbl = (SchemaObject) fkObjDef[2 + sourceCols.size()];
				if (!tTbl.getValue().equals(target.getValue())) {
					continue;
				}

				boolean isOk = true;
				for (int j = 0; j < sourceCols.size(); j++) {
					SchemaObject sCol = (SchemaObject) fkObjDef[2 + j];
					if (!sourceCols.contains(sCol.getValue())) {
						isOk = false;
						break;
					}

					SchemaObject tCol = (SchemaObject) fkObjDef[3 + sourceCols.size() + j];
					if (!targetCols.contains(tCol.getValue())) {
						isOk = false;
						break;
					}
				}

				if (!isOk) {
					continue;
				}

				fk1 = fkObj;
				break;
			}
		}

		return fk1;
	}

	/**
	 * Private constructor to avoid instantiating the class
	 */
	private AutoMedUtils() {
	}
}
